Aufgabenstellung: _Formulieren Sie ein Ziel oder mehrere Ziele nach dem CRISP-DM Prozess, die für Immobilienspekulant*innen sinnvoll sind. Bei Spekulationen werden typischerweise Immobilien erstanden, die wieder mit Gewinn abgestoßen werden. Beginnen Sie mit der Idee „Wir brauchen mehr Verständnis des Verkaufspreises (ZVerkaufspreis)!“. Geben Sie Ihre Ziele in Ihrem Jupyter-Notebook als Markup an (max. ½ Seite). Wichtig ist hier, eigene zu untersuchende Hypothesen aufzustellen, die dann in Aufgabenteil 2 untersucht werden. Nutzen Sie auch die vorhandenen Daten, um die Hypothesen zu ergänzen oder anzupassen, wenn notwendig.
Als Immobilienspekulant*innen bezeichnet man Personen, die eine Vielzahl von Immobilien erwerben und darauf hoffen, dass der Preis der Immobilien in Zukunft steigen wird. Die Häuser oder Wohnungen können dann mit Gewinn verkauft werden. Für die Kaufentscheidung und die einfache Bewertung der Attraktivität einer Immobilie, stehen für diese Personengruppe folgende Ziele und Anforderungen im Vordergrund:
Ein Haupziel auf dem Bereich der Geschäftsebene ist es, den Entscheidungsprozess für die Bewertung und den Kauf von Immobilien zu unterstützen. Durch die Anlyse der Daten sollen Vorhersagen zur Attraktivität der Angebote und die Auswirkungen der einzelnen Attribute (Parameter) auf den veranschlagten Preis bestimmt werden. Das Ergebnis sollte auch für nicht DataScience-kundige Anwender*innen verständlich und aussagekräftig gestaltet sein. Die zu beantortenden Fragen wurden als Ziele für die Untersuchung definiert (siehe vorherigen Abschnitt). Die Ergebnisse lassen sich in zwei Bereiche einteilen:
Aufgabenstellung: _Laden und untersuchen Sie den Datensatz in data_fortraining.csv nach den Regeln wie in der Vorlesung gelehrt. Nutzen Sie Mark-Up, um wichtige Erkenntnisse zu dokumentieren.
# Import modules and packages
import os
import numpy as np
import pandas as pd
import sklearn as sk
import seaborn as sns
import matplotlib.pyplot as plt
# Import training and test data
df_train = pd.read_csv("data_for_training.csv", delimiter=";").drop(columns="A_Index")
df_test = pd.read_csv("data_for_test.csv", delimiter=";").drop(columns="A_Index")
# Output training dataframe
df_train
| AnzahlZimmer | Ausbaustufe | Baeder | BaederKG | Baujahr | EG_qm | Garage_qm | Garagen | Gesamteindruck | Keller_Typ_qm | Keller_qm | Kellerhoehe | Kellertyp | Lage | OG_qm | Umgebaut | Verkaufsjahr | Verkaufsmonat | Wohnflaeche_qm | Z_Verkaufspreis | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 3 | 1 Ebene | 2 | 1 | 1992 | 125 | 49 | 2 | 3 | 88 | 116 | Gut | Guter Wohnraum | Bezirk 19 | 0 | 1992 | 2021 | 6 | 125 | 187500 |
| 1 | 2 | 1 Ebene | 2 | 1 | 2010 | 170 | 79 | 3 | 3 | 141 | 168 | Gut | Guter Wohnraum | Bezirk 16 | 0 | 2010 | 2020 | 7 | 170 | 350000 |
| 2 | 2 | 1 Ebene | 2 | 0 | 2015 | 119 | 40 | 2 | 3 | 0 | 119 | Gut | Rohbau | Bezirk 18 | 0 | 2015 | 2018 | 3 | 119 | 171750 |
| 3 | 2 | 2 Ebenen | 3 | 1 | 2015 | 64 | 40 | 2 | 3 | 48 | 64 | Gut | Guter Wohnraum | Bezirk 18 | 73 | 2016 | 2020 | 10 | 138 | 154000 |
| 4 | 3 | 1 Ebene | 2 | 0 | 2021 | 103 | 39 | 2 | 3 | 3 | 103 | Gut | Guter Wohnraum | Bezirk 8 | 0 | 2021 | 2022 | 3 | 103 | 213899 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2337 | 3 | 1 Ebene | 2 | 1 | 1989 | 109 | 40 | 2 | 4 | 57 | 105 | Durchschnitt | Mittlerer Wohnraum | Bezirk 14 | 0 | 1989 | 2022 | 5 | 109 | 218499 |
| 2338 | 3 | 1 Ebene | 2 | 1 | 1969 | 153 | 41 | 2 | 3 | 14 | 96 | Durchschnitt | Freizeitraum | Bezirk 22 | 0 | 2012 | 2018 | 1 | 153 | 155000 |
| 2339 | 3 | 2 Ebenen | 3 | 0 | 1997 | 83 | 40 | 2 | 3 | 0 | 62 | Gut | Rohbau | Bezirk 23 | 64 | 1997 | 2022 | 6 | 147 | 204699 |
| 2340 | 3 | 2 Ebenen | 2 | 0 | 1984 | 46 | 21 | 1 | 3 | 21 | 46 | Durchschnitt | Kein Wohnraum | Bezirk 13 | 46 | 1984 | 2019 | 5 | 92 | 85500 |
| 2341 | 3 | 2 Ebenen | 3 | 1 | 2008 | 117 | 70 | 3 | 3 | 61 | 117 | Gut | Guter Wohnraum | Bezirk 16 | 86 | 2009 | 2019 | 8 | 203 | 315750 |
2342 rows × 20 columns
# Output data types and non-null count of the individual columns
df_train.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 2342 entries, 0 to 2341 Data columns (total 20 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 AnzahlZimmer 2342 non-null int64 1 Ausbaustufe 2342 non-null object 2 Baeder 2342 non-null int64 3 BaederKG 2342 non-null int64 4 Baujahr 2342 non-null int64 5 EG_qm 2342 non-null int64 6 Garage_qm 2342 non-null int64 7 Garagen 2342 non-null int64 8 Gesamteindruck 2342 non-null int64 9 Keller_Typ_qm 2342 non-null int64 10 Keller_qm 2342 non-null int64 11 Kellerhoehe 2342 non-null object 12 Kellertyp 2342 non-null object 13 Lage 2342 non-null object 14 OG_qm 2342 non-null int64 15 Umgebaut 2342 non-null int64 16 Verkaufsjahr 2342 non-null int64 17 Verkaufsmonat 2342 non-null int64 18 Wohnflaeche_qm 2342 non-null int64 19 Z_Verkaufspreis 2342 non-null int64 dtypes: int64(16), object(4) memory usage: 366.1+ KB
# Show all individual entry names of the found object columns
for col in df_train.dtypes[df_train.dtypes == "object"].index:
print(f"{col.rstrip()}: {df_train.loc[:, col].unique()}\n")
Ausbaustufe: ['1 Ebene' '2 Ebenen' '3 Ebenen'] Kellerhoehe: ['Gut' 'Durchschnitt' '0' 'Sehr gut' 'Schlecht' 'Sehr Schlecht'] Kellertyp: ['Guter Wohnraum' 'Rohbau' 'Mittlerer Wohnraum' 'Niedrige Qualität' '0' 'Freizeitraum' 'Kein Wohnraum'] Lage: ['Bezirk 19' 'Bezirk 16' 'Bezirk 18' 'Bezirk 8' 'Bezirk 17' 'Bezirk 6' 'Bezirk 23' 'Bezirk 9' 'Bezirk 15' 'Bezirk 20' 'Bezirk 14' 'Bezirk 1' 'Bezirk 24' 'Bezirk 21' 'Bezirk 22' 'Bezirk 7' 'Bezirk 4' 'Bezirk 25' 'Bezirk 5' 'Bezirk 26' 'Bezirk 27' 'Bezirk 12' 'Bezirk 2' 'Bezirk 13' 'Bezirk 10' 'Bezirk 3' 'Bezirk 11' '0']
Über die describe() Funktion von pandas kann eine Übersicht mit Infos zu zentralen Tendenzen, Streuung und Verteilung des Datensatzes ausgegeben werden.
df_train.describe()
| AnzahlZimmer | Baeder | BaederKG | Baujahr | EG_qm | Garage_qm | Garagen | Gesamteindruck | Keller_Typ_qm | Keller_qm | OG_qm | Umgebaut | Verkaufsjahr | Verkaufsmonat | Wohnflaeche_qm | Z_Verkaufspreis | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 | 2342.000000 |
| mean | 2.855252 | 1.906063 | 0.491460 | 1980.755337 | 95.856106 | 38.290777 | 1.707088 | 3.454313 | 36.761742 | 86.237404 | 28.536721 | 1994.701537 | 2019.867635 | 6.127669 | 125.014944 | 180443.509394 |
| std | 0.823598 | 0.806672 | 0.533111 | 29.626630 | 31.630087 | 17.389905 | 0.743206 | 0.663509 | 35.738675 | 34.912682 | 35.734942 | 20.426072 | 1.318058 | 2.672245 | 41.589194 | 77722.391308 |
| min | 0.000000 | 0.000000 | 0.000000 | 1884.000000 | 28.000000 | 0.000000 | 0.000000 | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 1962.000000 | 2018.000000 | 1.000000 | 28.000000 | 13100.000000 |
| 25% | 2.000000 | 1.000000 | 0.000000 | 1964.000000 | 73.000000 | 26.000000 | 1.000000 | 3.000000 | 0.000000 | 66.000000 | 0.000000 | 1976.000000 | 2019.000000 | 4.000000 | 93.000000 | 130000.000000 |
| 50% | 3.000000 | 2.000000 | 0.000000 | 1982.000000 | 89.000000 | 39.000000 | 2.000000 | 3.000000 | 32.000000 | 81.500000 | 0.000000 | 2002.000000 | 2020.000000 | 6.000000 | 121.000000 | 161950.000000 |
| 75% | 3.000000 | 2.000000 | 1.000000 | 2009.000000 | 114.000000 | 48.000000 | 2.000000 | 4.000000 | 61.000000 | 105.000000 | 59.000000 | 2014.000000 | 2021.000000 | 8.000000 | 146.000000 | 213882.000000 |
| max | 8.000000 | 6.000000 | 3.000000 | 2022.000000 | 324.000000 | 126.000000 | 5.000000 | 5.000000 | 194.000000 | 272.000000 | 175.000000 | 2022.000000 | 2022.000000 | 12.000000 | 380.000000 | 755000.000000 |
Die Untersuchung der Rohdaten liefert bereits erste Einblicke und mögliche Ansätze für die weitere Datenexploration. Für die spätere Vorhersage des Preises ist bereits interessant, dass die Spanne von 13.100 € bis 755.000 € sehr groß ist.
Die Verteilung und Häufigkeit des Verkaufspreises wird nach dieser ersten Eingrenzung nun genauer analysiert.
plt.figure(figsize=(20,5))
# Violin plot
plt.subplot(1,2,1)
plt.title("Verteilung des Verkaufspreises")
sns.violinplot(data=df_train, y=df_train["Z_Verkaufspreis"])
# Histogram plot
plt.subplot(1,2,2)
plt.title("Histogramm über den Verkaufspreis")
plt.xlabel("Verkaufspreis in Euro")
plt.ylabel("Anzahl der Häuser")
df_train["Z_Verkaufspreis"].hist(bins=100)
plt.show()
Die meisten Immobilien des Datensatzes befinden sich im Bereich des Durchschnitssverkaufspreises (180.443.51 €) und werden in einer Preisspanne von 100.000 € bis 130.000 € verkauft. Die günstigste Immobilien kostet 13.100 € und die teuerste 755.000 €. Einige wenige Häuser sind preislich deutlich über dem Durchschnitt angesiedelt.
Anzeige einer Scatterplotmatrix, um die Zusammenhänge von ausgewählten Attributen zueinander zu untersuchen.
sns.pairplot(df_train.loc[:, ["Keller_qm", "EG_qm", "OG_qm", "Wohnflaeche_qm", "Garage_qm", "Baujahr", "Umgebaut"]])
plt.show()
Keller_qm, EG_qm, OG_qm, Wohnflaeche_qm) sind überwiegend normalverteilt. Zudem ist eine Verschiebung der Verteilung zu erkennen.Umgebaut im Datensatz dar.
Es lässt sich ein starker Zusammenhang zwischen Keller_qm und EG_qm beobachten. Die Beziehung zwischen EG_qm und OG_qm ist hingegen schwach bis kaum ausgeprägt.
Die Winkelhalbierende zwischen EG_qm und Wohnflaeche_qm repräsentiert diejenigen Häuser, die nur aus einem Erdgeschoss (und evtl. einem Keller) bestehen.
Bis auf zwei Häuser besitzen alle Immobilien mit einer Wohnflaeche_qm über 200 m² einen Keller.
Alle Häuser besitzen ein älteres Baujahr als Umbaujahr, was logisch sinnvoll ist.
Über die Jahre (Baujahr) wurden Keller_qm, EG_qm und Garage_qm tendenziell zunehmend größer gebaut.
color_palette = sns.color_palette("Set2", 5)
sns.pairplot(df_train.loc[:, ["Keller_qm", "EG_qm", "OG_qm", "Wohnflaeche_qm", "Garage_qm", "Baujahr", "Umgebaut", "Gesamteindruck"]], hue="Gesamteindruck", palette=color_palette)
plt.show()
Nimmt man das Attribut Gesamteindruck zur bestehenden Scatterplotmatrix hinzu, so fällt auf, dass der Gesamteindruck sehr gemischt auftritt. Die größte Korrelation besteht mit dem Baujahr. Die Immobilien, die in den letzten zwei Jahrzenten erbaut wurden, werden nahezu alle mit einem Gesamteindruck von 5 (Sehr gut) bewertet.
Dieses Phänomen lässt sich dadurch erklären, dass der Gesamteindruck ein sehr subjektives Attribut ist, das von jedem Menschen anders bewertet werden kann. Ob und inwiefern sich der Gesamteindruck für die Vorhersage des Verkaufspreises eignet, wird im weiteren Verlauf der Aufgabe untersucht.
Im ersten Schritt werden nur die numerischen Features des Datensatzes berücksichtigt. Nach der abgeschlossenen Data Preparation (Aufgabe 3) wird dann die Korrelationsmatrix als Heatmap erneut über alle (encodierten) Attribute ausgegeben und verglichen.
# Draw a correlation matrix heatmap and mask out the upper triangle
plt.figure(figsize=(20, 10))
mask = np.triu(np.ones_like(df_train.iloc[:, 0:20].corr(numeric_only=True), dtype=bool))
heatmap = sns.heatmap(df_train.corr(numeric_only=True), mask=mask, vmin=-1, vmax=1, annot=True)
plt.title('Korrelations Heatmap mit numerischen Features', fontdict={'fontsize': 20})
plt.show()
Analyse von auffälligen Korrelationen:
Garagen) sowie die Garagengröße (Garage_qm). Ein kausaler, baulich bedingter Zusammenhang ist erkennbar.Keller_qm und EQ_qm sowie Wohnfläche_qm und Baeder korrelieren ebenfalls sehr stark. Ein kausaler Zusammenhang ist ebenfalls erkennbar.Gesamteindruck und Baujahr. Daran lässt sich erkennen, dass eine Verringerung des Baujahrs einen schlechteren (kleineren) Gesamteindruck zur Folge hat und umgekehrt.AnzahlZimmer korreliert. Instinktiv würde man hier einen Zusammenhang vermuten, da die Zimmeranzahl für viele Immobilieninteressenten in der realen Welt ein ausschlaggebendes Kriterium zu sein scheint.Verkaufsmonat und Verkaufsjahr.Z_Verkaufspreis und Verkaufsjahr korrelieren nicht, obwohl man annehmen würde, das durch die Inflation die Verkaufspreise der Immobilien über die Jahre steigen müssten.Die Korrelationsmatrix zeigt, dass der Verkaufspreis (Z_Verkaufspreis) von folgenden Attributen abhängig ist:
Baeder: 0,54Baujahr: 0,55EG_qm: 0,62Garage_qm: 0,62Garagen: 0,62Keller_qm: 0,64Umgebaut: 0,52Wohnflaeche_qm: 0,69
Um die Untersuchung zu erleichtern, werden zuerst Funktionen erstellt, die Visualisierungen zwischen einem ausgewählten Attribut und dem Verkaufspreis ermöglichen.
# Function to display a scatterplot
def display_scatterplot(attribute, xlabel):
# Create graph
plt.figure(figsize=(14, 7))
plt.scatter(df_train[attribute], df_train["Z_Verkaufspreis"])
# Add and display captions
plt.title(f"Verkaufspreis vs. {xlabel}")
plt.xlabel(xlabel)
plt.ylabel("Verkaufspreis in Euro")
plt.show()
# Function to display a boxplot
def display_boxplot(attribute, xlabel, title, rotate_xlabel=False):
# Create graph
plt.figure(figsize=(14, 7))
sns.boxplot(x=attribute, y="Z_Verkaufspreis", data=df_train, color="orange")
# Add and display captions
plt.title(f"Einfluss {title} auf den Verkaufspreis")
plt.xlabel(xlabel)
if rotate_xlabel == True: plt.xticks(rotation = 45)
plt.ylabel("Verkaufspreis in Euro")
plt.show()
display_boxplot(attribute="AnzahlZimmer", xlabel="Gesamtanzahl der Zimmer (ohne Bäder)", title="der Zimmeranzahl")
Die Korrelationsmatrix hat bereits gezeigt, dass es einen überraschend geringen Zusammenhang zwischen der Anzahl der Zimmer und dem Verkaufspreis gibt. Die erwartete, stetige Steigerung des Verkaufspreises in Abhängigkeit von der Zimmeranzahl bleibt hier aus. Häuser mit zwei Zimmern kosten im Durchschnitt ähnlich viel wie Häuser mit sechs Zimmern. Der einzelne Dateneintrag mit acht Zimmern ist nicht repräsentativ und kann später als Ausreißer entfernt werden. Des weiteren sind die Immobilien mit 0 Zimmern (Kellerwohnungen) nicht repräsentativ und können in Aufgabe 3 ebenfalls entfernt werden.
display_boxplot(attribute="Ausbaustufe", xlabel="Anzahl der Ebenen oberhalb des Kellers", title="der Ausbaustufe")
Es gibt keinen wesentlichen Zusammenhang zwischen der Ausbaustufe und dem Verkaufspreis.
display_boxplot(attribute="Baeder", xlabel="Anzahl Bäder im EG und OG", title="der Bäderanzahl (EG & OG)")
Bei den Immobilien mit ein bis vier Bädern ist ein starker Anstieg des Verkaufspreises mit der Anzahl an Bädern zu erkennen. Bei 6 Bädern handelt es sich mit 4 Einträgen um Ausreißer. Später werden ebenfalls alle Häuser mit 0 Bädern im EG entfernt (3 Einträge) da diese nicht repräsentativ sind.
display_boxplot(attribute="BaederKG", xlabel="Anzahl Bäder im KG", title="der Bäderanzahl (KG)")
Insgesamt ist ein starker Zusammenhang zwischen der Anzahl an Bädern im UG und dem Verkaufspreis zu erkennen, jedoch nicht so stark bei bei der Anzahl an Bädern in den oberen Geschossen.
display_scatterplot(attribute="Baujahr", xlabel="Baujahr der Immobilie")
Hier lässt sich gut erkennen, dass neuere Häuser tendenziell einen höheren Verkaufspreis haben als ältere.
display_scatterplot(attribute="EG_qm", xlabel="Ergeschossgröße in m²")
Die starke Korrelation zwischen EG_qm und Z_Verkaufspreis aus der obigen Korrelationmatrix wird durch diesen Scatterplot visualisiert. Die Ausreißer bei EG_qm > 250 werden in Aufgabe 3 entfernt.
display_scatterplot(attribute="Garage_qm", xlabel="Garagengröße in m²")
Auch hier lässt sich wie bei der Erdgeschossgröße schon ein Zusammenhang zwischen der Garagengröße und dem Verkaufspreis erkennen. Die Einträge mit einer Flächengröße von 0 bedeuten, dass manche Immobilien keine Garage besitzen.
display_boxplot(attribute="Garagen", xlabel="Anzahl an Garagenstellplätze", title="der Anzahl an Garagenstellplätze")
sns.countplot(data=df_train, x="Garagen")
plt.title("Verteilung der Garagenanzahl über den Datensatz")
plt.show()
Die meisten Häuser besitzen zwei Garagenstellplätze und befinden sich im mittleren Preissegment. Von einer bis drei Garagen ist im Boxplot bezüglich Median und Maximum ein deutlicher Verlauf zu erkennen. Diese Beobachtung stimmt mit der Korrelation in der Heatmap überein.
Des Weiteren fällt auf, dass ein Haus mit einer Garage nur eine geringe Wertsteigerung im Vergleich zu einem Haus mit keiner Garage erfährt. Jedoch ist ein Haus mit zwei Garagen im Median ca. 50.000 € mehr Wert als ein Haus mit einer Garage. Ein Haus mit drei Garagen ist im Median sogar ca. 100.000 € mehr Wert als ein Haus mit zwei Garagen.
Der Verkaufspreis von Häusern mit vier Garagen ist aber erstaunlicherweise deutlich geringer als der Preis bei drei Garagen. Dieser Verstoß gegen den Trend könnte daran liegen, dass es nur 13 Immobilien mit vier Garagen im Datensatz gibt. Da es nur einen Eintrag mit fünf Garagen gibt, kann dieser als Ausreißer entfernt werden.
color_palette = sns.color_palette("Set2", 6)
sns.lineplot(data=df_train, x="Wohnflaeche_qm", y="Z_Verkaufspreis", hue="Garagen", palette=color_palette)
plt.title("Zusammenhang von Garagen, Wohnfläche_qm und Verkaufspreis")
plt.show()
Anhand des LinePlots soll untersucht werden, ob die Häuser mit vielen Garagen einfach nur größer und damit teurer sind (Wohnfläche_qm) oder ob die Garagen an sich die Häuser wertvoller machen. Betrachtet man nur die Häuser mit einer Wohnfläche unter 250 m², für die auch eine quantitive Aussage getroffen werden kann, so erkennt man, dass die Garagen nur einen relativ kleinen Einfluss haben. Der Verkaufspreis hängt nur transitiv von den Garagen über die Wohnfläche ab. In größeren Häusern wohnen meist auch mehr Personen, wodurch sich die größere Garagenanzahl erklärt.
display_boxplot(attribute="Gesamteindruck", xlabel="Gesamteindruck der Immobilie", title="des Gesamteindrucks")
Aus der obigen Heatmap geht bereits hervor, dass es keinen Zusammenhang zwischen Gesamteindruck und Verkaufspreis gibt. Der Boxplot verdeutlicht, dass anhand des Medians kein aufsteigender Trend im Verkaufspreis mit den Bewertungen zu erkennen ist.
display_scatterplot(attribute="Keller_Typ_qm", xlabel="Kellertypgröße in m²")
Auch bei der Kellertypgröße ist wie schon bei der gesamten Kellergröße (Keller_qm) ein Zusammenhang zum Verkaufspreis zu erkennen. Die Korrelation fällt allerdings schwächer aus. Einträge mit 0 m² lassen darauf schließen, dass die Immobilie keinen Keller besitzt.
display_scatterplot(attribute="Keller_qm", xlabel="Kellergröße in m²")
Dieser Plot ist annähernd deckungsgleich mit dem Scatterplot über EG_qm. Dies ist keine Überraschung, da beide Attribute eine Korrelation von 0,77 haben.
Im Gegensatz zur Erdgeschossfläche gibt es hier allerdings viele Einträge mit 0 m² Kellerfläche. Dies bedeutet, dass die Häuser keinen Keller besitzen.
display_boxplot(attribute="Kellerhoehe", xlabel="Höhe des Kellers", title="der Kellerhoehe")
Anhand des Boxplots ist der Zusammenhang zwichen Kellerhöhe und Verkaufspreis ersichtlich. An den Medianen lässt sich ein aufsteigender Trend von Sehr Schlecht bis Sehr gut erkennen. Höhere Keller (Sehr gut: ca. 250 cm) sorgen im Mittel für höhere Verkaufspreise.
display_boxplot(attribute="Kellertyp", xlabel="Typ des Kellers", title="des Kellertyps")
Es ist kein wesentlicher Zusammenhang zwischen dem Kellertyp und dem Verkaufpreis ersichtlich.
display_boxplot(attribute="Lage", xlabel="Bezirk in dem die Immobilie steht", title="der Lage", rotate_xlabel=True)
Auch die Lage wirkt sich auf den Preis aus. Dabei fällt vor allem auf, dass es Bezirke gibt, in denen die Immobilien teuer sind (z.B. Bezirk 16, Bezirk 11 und Bezirk 18). Dem gegenüber sind z.B. Bezirk 13 und Bezirk 3 eher billigere Wohngegenden.
Es muss jedoch überprüft werden, ob die unterschiedliche Bewertung rein an der Lage liegt, oder eher daran, dass die Häuser in manchen Bezirken einfach neuer (Baujahr) oder größer (Wohnflaeche_qm) und dadurch teurer sind.
plt.figure(figsize=(20,5))
# Lage - Baujahr
plt.subplot(1,2,1)
ax = sns.boxplot(data=df_train, x="Lage", y="Baujahr")
plt.xticks(rotation=45)
plt.title("Lage unter Betrachtung des Baujahrs")
# Lage - Wohnflaeche_qm
plt.subplot(1,2,2)
ax = sns.boxplot(data=df_train, x="Lage", y="Wohnflaeche_qm")
plt.xticks(rotation=45)
plt.title("Lage unter Betrachtung der Wohnflächengröße")
plt.show()
Lage unter Betrachtung des Baujahrs:
Die teuersten Bezirke (Bezirk 16, Bezirk 11 und Bezirk 18) sind tatsächlich Gegenden mit vielen neuen Häusern. Jedoch gibt es auch Gegenden wie Bezirk 1 und Bezirk 24, die neuere oder genau so neue Häuser haben, aber bei weitem nicht so teuer sind. Es ist also keine Korrelation zwischen Lage und Baujahr gegeben.
Lage unter Betrachtung der Wohnflächengröße:
Die teuren Gegenden haben nicht zwingend auch die größte Wohnfläche. Bei Bezirk 16 trifft es zu, aber Bezirk 11 hat beispielsweise eine der kleinsten Wohnflächen trotz des hohen Preises. Es ist also keine Korrelation zwischen Lage und Wohnflächengröße gegeben.
display_scatterplot(attribute="OG_qm", xlabel="Obergeschossgröße in m²")
Am Scatterplot fällt zunächst ins Auge, dass der aller größte Teil der Immobilien im Datensatz, ca. 56 %, kein(e) Obergeschoss(e) besitzt. Für die übrigen Einträge ist ein starke Zusammenhang erkennbar. Da aber der überwiegende Teil der Einträge für dieses Attribut 0 ist, beträgt die Korrelation in der Heatmap zwischen OG_qm und Z_Verkaufspreis nur 0,26.
Da das Attribut Wohnflaeche_qm unter anderem bereits den Wert der Obergeschossgröße enthält, sollte für die späteren Vorhersagen das Attribut Wohnflaeche_qm verwendet werden. Eine gleichzeitige Betrachtung beider Features brächte Redundanz.
display_scatterplot(attribute="Umgebaut", xlabel="Jahr des Umbaus der Immobilie")
Dieser Scatterplot ähnelt dem obigen über das Baujahr stark und verdeutlicht den Zusammenhang zwischen Verkaufspreis und dem Jahr noch deutlicher. Immobilien, die im obigen Graphen herausstechen (z.B. 475.000 € bei Baujahr 1907, Umgebaut 2005) sind im Umgebaut-Plot kaum noch vorhanden. Daraus lässt sich schließen, dass die Renovierung den Wert eines Hauses steigert. Außerdem bestätigt sich der Zusammenhang "Neuere Häuser sind teurer" auch für alte, aber umgebaute Häuser.
display_boxplot(attribute="Verkaufsjahr", xlabel="Jahr des Verkaufs", title="des Verkaufsjahres")
Hier ist ein leichter Aufwärtstrend zu erkennen. Häuser, die später verkauft wurden, wurden in der Regel etwas teurer verkauft. Dies hängt wahrscheinlich mit allgemein steigenden Immobilienpreisen und/oder der Inflation zusammen. Zur Klärung des Sachverhalts wäre eine Rückfrage an Expert*innen hilfreich.
display_boxplot(attribute="Verkaufsmonat", xlabel="Monat des Verkaufs", title="des Verkaufsmonats")
# Generate the histogram
df_train["Verkaufsmonat"].hist(bins=12)
# Title and axis labels
plt.title("Anzahl der verkauften Immobilien nach Monat")
plt.xlabel("Verkaufsmonat")
plt.ylabel("Anzahl der Immobilien")
plt.show()
Es gibt keinen starken Zusammenhang zwischen Verkaufsmonat und Verkaufspreis. Anhand des Histogramms erkennt man, dass die meisten Verkäufe in der ersten Jahreshälfte, genauer gesagt von April bis Juli stattfinden. Jedoch werden im Median die Häuser am teuersten im Januar verkauft. Außerdem gibt es in den Sommermonaten Mai bis Juli die meisten Ausreiser. Man kann also davon ausgehen, dass in diesen Monaten eher sehr hochpreisige Häuser verkauft werden.
display_scatterplot(attribute="Wohnflaeche_qm", xlabel="Wohnflächengröße in m²")
Da sich die Wohnfläche unter anderem aus EG_qm und Keller_qm zusammensetzt, ist hier ein ähnlicher Zusammenhang zum Verkaufspreis zu erkennen. Die Größe der Wohnfläche korreliert starkt mit dem Verkaufspreis. Je mehr Wohnfläche ein Haus bietet, desto teurer kann es verkauft werden.
Zentrale Erkenntnisse:
Wohnflaeche_qm: 0,69Keller_qm: 0,64EG_qm: 0,62Garage_qm: 0,62Garagen: 0,62Baujahr: 0,55Baeder: 0,54Umgebaut: 0,52Wohnfläche_qm oder Bau weiterer Baeder) oder baulich nicht möglich (z.B. Vergrößerung der Kellerhoehe).Expert*innen-Rückfragen
Weitere Datenquellen:
Aufgabenstellung: Bereinigen Sie die Daten und führen Sie Feature Engineering durch. Hinweis: Kann bereits für Aufgabe 2 teilweise notwendig sein, dann kenntlich machen und zusammenfassend aufführen.
Im Bereich der Datenvorbereitung ist die Kodierung kategorischer Daten eine zentrale Aufgabe. Die meisten Daten im realen Leben bestehen bekannterweise aus kategorischen String-Werten. Für die computergestützte Verarbeitung benötigen die Modelle jedoch Fließkommazahlen oder ganze Zahlen.
# Show all columns which need to be encoded
columns_object = df_train.dtypes[df_train.dtypes == "object"].index
print(columns_object, "\n----------")
# Show all individual entry names of the found columns
for col in columns_object:
print(f"{col.rstrip()}: {df_train.loc[:, col].unique()}\n")
# Create a copy of the Training DataFrame
df_train_cleaned = df_train.copy(deep=True)
Index(['Ausbaustufe', 'Kellerhoehe', 'Kellertyp', 'Lage'], dtype='object') ---------- Ausbaustufe: ['1 Ebene' '2 Ebenen' '3 Ebenen'] Kellerhoehe: ['Gut' 'Durchschnitt' '0' 'Sehr gut' 'Schlecht' 'Sehr Schlecht'] Kellertyp: ['Guter Wohnraum' 'Rohbau' 'Mittlerer Wohnraum' 'Niedrige Qualität' '0' 'Freizeitraum' 'Kein Wohnraum'] Lage: ['Bezirk 19' 'Bezirk 16' 'Bezirk 18' 'Bezirk 8' 'Bezirk 17' 'Bezirk 6' 'Bezirk 23' 'Bezirk 9' 'Bezirk 15' 'Bezirk 20' 'Bezirk 14' 'Bezirk 1' 'Bezirk 24' 'Bezirk 21' 'Bezirk 22' 'Bezirk 7' 'Bezirk 4' 'Bezirk 25' 'Bezirk 5' 'Bezirk 26' 'Bezirk 27' 'Bezirk 12' 'Bezirk 2' 'Bezirk 13' 'Bezirk 10' 'Bezirk 3' 'Bezirk 11' '0']
Labelencoding durchführen um die Ebenenangebe in ganzzahlige Werte umzuwandeln.
Ordinalskala von 1 bis 3
3: 3 Ebenen
2: 2 Ebenen
1: 1 Ebene
# Define mapping, apply labelencoding and output for verification
mapping = {"1 Ebene": 1, "2 Ebenen": 2, "3 Ebenen": 3}
df_train_cleaned["Ausbaustufe"] = df_train_cleaned["Ausbaustufe"].replace(mapping)
df_train_cleaned[['Ausbaustufe']].head()
| Ausbaustufe | |
|---|---|
| 0 | 1 |
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
| 4 | 1 |
Labelencoding durchführen um die Bewertung der Kellerhöhe (Werte auf der Skala von "Sehr schlecht" bis "Sehr gut") in ganzzahlige Werte umzuwandeln. Die Einträge mit Nullwerten bedeuten, dass die Immobilie keinen Keller hat.
Ordinalskala von 0 bis 5:
5: Sehr gut - ca. 250 cm
4: Gut - ca. 225 cm
3: Durchschnitt - ca. 200 cm
2: Schlecht - ca. 175 cm
1: Sehr schlecht - niedriger als 175 cm
0: Keine Angabe - kein Keller
# Define mapping, apply labelencoding and output for verification
mapping = {"0": 0, "Sehr Schlecht": 1, "Schlecht": 2, "Durchschnitt": 3, "Gut": 4, "Sehr gut": 5}
df_train_cleaned["Kellerhoehe"] = df_train_cleaned["Kellerhoehe"].replace(mapping)
df_train_cleaned[['Kellerhoehe']].head()
| Kellerhoehe | |
|---|---|
| 0 | 4 |
| 1 | 4 |
| 2 | 4 |
| 3 | 4 |
| 4 | 4 |
Für den Kellertyp wird ein Label Encoding durchgeführt. Das Mapping und die Rangfolge basieren dabei auf den Angaben in der Aufgabenstellung. Die Einträge mit Nullwerten bedeuten, dass die Immobilie keinen Keller hat.
Nominalskala von 0 bis 6:
6: Guter Wohnraum
5: Mittlerer Wohnraum
4: Kein Wohnraum
3: Freizeitraum
2: Niedrige Qualität
1: Rohbau
0: Keine Angabe - kein Keller
# Define mapping, apply labelencoding and output for verification
mapping = {"0": 0, "Rohbau": 1, "Niedrige Qualität": 2, "Freizeitraum": 3, "Kein Wohnraum": 4, "Mittlerer Wohnraum": 5, "Guter Wohnraum": 6}
df_train_cleaned["Kellertyp"] = df_train_cleaned["Kellertyp"].replace(mapping)
df_train_cleaned[['Kellertyp']].head()
| Kellertyp | |
|---|---|
| 0 | 6 |
| 1 | 6 |
| 2 | 1 |
| 3 | 6 |
| 4 | 6 |
One-Hot Encoding durchführen um die Lage in ganzzahlige Werte umzuwandeln. Die Immobilien bei denen der Bezirk nicht angegeben ist (Nullwerte) werden entfernt, da sie sich sonst negativ auf die Vorhersage des Verkaufspreises auswirken.
# Delete entries with null values
# This deletion is completed by "selecting" rows where "Lage" numbers are non zero
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["Lage"] != '0']
# Define mapping and apply labelencoding to get shorter column names
mapping = {
"Bezirk 1": 1, "Bezirk 2": 2, "Bezirk 3": 3, "Bezirk 4": 4, "Bezirk 5": 5, "Bezirk 6": 6, "Bezirk 7": 7, "Bezirk 8": 8, "Bezirk 9": 9, "Bezirk 10": 10,
"Bezirk 11": 11, "Bezirk 12": 12, "Bezirk 13": 13, "Bezirk 14": 14, "Bezirk 15": 15, "Bezirk 16": 16, "Bezirk 17": 17, "Bezirk 18": 18, "Bezirk 19": 19, "Bezirk 20": 20,
"Bezirk 21": 21, "Bezirk 22": 22, "Bezirk 23": 23, "Bezirk 24": 24, "Bezirk 25": 25, "Bezirk 26": 26, "Bezirk 27": 27
}
df_train_cleaned["Lage"] = df_train_cleaned["Lage"].replace(mapping)
# Apply One-Hot Encoding and output for verification
encoder_lage = pd.get_dummies(df_train_cleaned["Lage"], prefix="Lage")
df_train_cleaned[encoder_lage.columns] = encoder_lage
#df_train_cleaned = df_train_cleaned.drop("Lage", axis=1) # optional: drop original "lage" column
df_train_cleaned.iloc[:, 20:47].head()
| Lage_1 | Lage_2 | Lage_3 | Lage_4 | Lage_5 | Lage_6 | Lage_7 | Lage_8 | Lage_9 | Lage_10 | ... | Lage_18 | Lage_19 | Lage_20 | Lage_21 | Lage_22 | Lage_23 | Lage_24 | Lage_25 | Lage_26 | Lage_27 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ... | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 4 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
5 rows × 27 columns
Bei Ausreißern handelt es sich um Werte, die nicht den Erwartungen entsprechen bzw. nicht zu den restlichen Werten der Verteilung passen. Viele Algorithmen reagieren schlecht auf Ausreißer. Es existiert keine klare Regel oder gar ein fester Schwellwert für die eindeutige Identifikation von Ausreißern. Welche Werte als Ausreißer gekennzeichnet und aus dem Datensatz entfernt werden, entscheidet der Data Scientist mithilfe von Fachwissen oder Experten.
Entferne alle Immobilien, die 0 Zimmer haben. Dabei kann es sich höchstens um Kellerwohnungen handeln, die nicht repräsentativ für den Datensatz sind.
# Delete entries with null values and display unique entries
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["AnzahlZimmer"] != 0]
df_train_cleaned.loc[:, "AnzahlZimmer"].unique()
array([3, 2, 5, 4, 1, 6, 8], dtype=int64)
Entferne alle Immobilien, die 0 Baeder bzw. Toiletten im Erdgeschoss haben. Diese Einträge sind nicht repräsentativ für den Datensatz.
# Delete entries with null values and display unique entries
df_train_cleaned = df_train_cleaned.loc[(df_train_cleaned["Baeder"] != 0)]
df_train_cleaned.loc[:, "Baeder"].unique()
array([2, 3, 1, 4, 6], dtype=int64)
Es existiert genau ein Eintrag mit 3 Bädern im Kellergeschoss. Dieser wird entfernt.
# Delete entries with 3 bathrooms and display unique entries
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["BaederKG"] != 3]
df_train_cleaned.loc[:, "BaederKG"].unique()
array([1, 0, 2], dtype=int64)
Es existiert genau ein Eintrag mit 5 Garagen. Dieser wird entfernt.
# Delete entries with 5 garages and display unique entries
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["Garagen"] != 5]
df_train_cleaned.loc[:, "Garagen"].unique()
array([2, 3, 1, 0, 4], dtype=int64)
Anhand eines Scatterplots prüfen, ob es starke Ausreiser bei der Größe der Wohnfläche im Erdgeschoss gibt. Falls vorhanden werden diese Einträge aus dem Datensatz entfernt.
# Draw scatterplot with red line to illustrate the outliers
plt.figure(figsize=(16, 7))
sns.scatterplot(data=df_train, x="EG_qm", y="Z_Verkaufspreis")
plt.plot([235, 235], [800000, 0], linewidth=2, color="red")
plt.title("Scatterplot über die unbereinigten EG_qm")
plt.xlabel("Größe der Wohnfläche im Erdgeschoss in qm")
plt.ylabel("Verkaufspreis in Euro")
Text(0, 0.5, 'Verkaufspreis in Euro')
# Delete all outliers from the data set and display unique entries
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["EG_qm"] <= 235]
df_train_cleaned.loc[:, "EG_qm"].unique()
array([125, 170, 119, 64, 103, 89, 75, 70, 80, 97, 120, 91, 78,
128, 117, 87, 62, 69, 115, 88, 68, 77, 94, 110, 106, 83,
142, 90, 104, 82, 143, 63, 73, 189, 145, 109, 84, 122, 67,
130, 99, 74, 81, 58, 155, 121, 66, 161, 44, 48, 57, 61,
98, 131, 92, 85, 173, 93, 127, 154, 100, 108, 141, 60, 53,
76, 126, 71, 102, 116, 111, 211, 175, 135, 164, 137, 124, 133,
45, 72, 86, 193, 65, 118, 146, 96, 46, 140, 51, 79, 214,
113, 151, 156, 138, 139, 129, 95, 107, 134, 101, 112, 132, 31,
168, 201, 205, 114, 37, 152, 147, 149, 54, 59, 153, 212, 123,
41, 179, 105, 158, 229, 165, 56, 47, 167, 223, 186, 34, 136,
52, 36, 181, 144, 209, 163, 171, 150, 148, 207, 172, 203, 157,
49, 187, 204, 50, 227, 200, 159, 192, 188, 43, 28, 42, 166,
174, 183, 180, 176, 208, 38, 231, 55, 162], dtype=int64)
Anhand eines Scatterplots prüfen, ob es starke Ausreiser bei der Größe der Garage gibt. Falls vorhanden werden diese Einträge aus dem Datensatz entfernt.
# Draw scatterplot with red line to illustrate the outliers
plt.figure(figsize=(16, 7))
sns.scatterplot(data=df_train, x="Garage_qm", y="Z_Verkaufspreis")
plt.plot([105, 105], [800000, 0], linewidth=2, color="red")
plt.title("Scatterplot über die unbereinigten Garage_qm")
plt.xlabel("Größe der Garage in qm")
plt.ylabel("Verkaufspreis in Euro")
Text(0, 0.5, 'Verkaufspreis in Euro')
# Delete all outliers from the data set and display unique entries
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["Garage_qm"] <= 105]
df_train_cleaned.loc[:, "Garage_qm"].unique()
array([ 49, 79, 40, 39, 27, 26, 53, 38, 24, 47, 41, 29, 33,
42, 23, 32, 0, 34, 55, 44, 66, 19, 68, 54, 50, 72,
28, 61, 22, 20, 52, 63, 37, 30, 43, 48, 25, 46, 18,
31, 73, 21, 74, 57, 17, 15, 65, 51, 67, 69, 81, 71,
35, 13, 70, 56, 78, 88, 45, 75, 36, 77, 58, 62, 64,
90, 82, 59, 97, 86, 76, 80, 8, 60, 83, 16, 85, 92,
93, 87, 104, 84], dtype=int64)
Anhand eines Scatterplots prüfen, ob es starke Ausreiser bei der Größe des Kellertyps (Keller_Typ_qm) gibt. Falls vorhanden werden diese Einträge aus dem Datensatz entfernt.
# Draw scatterplot with red line to illustrate the outliers
plt.figure(figsize=(16, 7))
sns.scatterplot(data=df_train, x="Keller_Typ_qm", y="Z_Verkaufspreis")
#plt.plot([160, 160], [800000, 0], linewidth=2, color="red")
plt.title("Scatterplot über die unbereinigten Keller_Typ_qm")
plt.xlabel("Anzahl der qm im Typ des Kellers")
plt.ylabel("Verkaufspreis in Euro")
Text(0, 0.5, 'Verkaufspreis in Euro')
Anhand eines Scatterplots prüfen, ob es starke Ausreiser bei der Größe des Kellers (Keller_qm) gibt. Falls vorhanden werden diese Einträge aus dem Datensatz entfernt.
# Draw scatterplot with red line to illustrate the outliers
plt.figure(figsize=(16, 7))
sns.scatterplot(data=df_train, x="Keller_qm", y="Z_Verkaufspreis")
plt.plot([265, 265], [800000, 0], linewidth=2, color="red")
plt.title("Scatterplot über die unbereinigten Keller_qm")
plt.xlabel("Anzahl der qm des gesamten Kellers")
plt.ylabel("Verkaufspreis in Euro")
Text(0, 0.5, 'Verkaufspreis in Euro')
# Delete all outliers from the data set and display unique entries
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["Keller_qm"] <= 265]
df_train_cleaned.loc[:, "Keller_qm"].unique()
array([116, 168, 119, 64, 103, 89, 75, 70, 78, 97, 56, 92, 76,
0, 61, 41, 53, 115, 91, 80, 90, 68, 79, 94, 110, 82,
69, 88, 129, 77, 133, 51, 73, 188, 145, 122, 57, 100, 125,
99, 95, 58, 158, 67, 106, 32, 66, 60, 44, 81, 48, 54,
83, 124, 72, 123, 74, 85, 120, 169, 93, 127, 154, 108, 84,
35, 143, 102, 59, 98, 140, 111, 208, 118, 172, 135, 147, 137,
62, 155, 18, 40, 139, 86, 126, 22, 144, 13, 96, 45, 146,
113, 46, 132, 214, 71, 105, 151, 156, 138, 134, 29, 149, 33,
87, 101, 130, 16, 142, 104, 109, 65, 49, 63, 20, 121, 112,
107, 153, 114, 24, 36, 42, 47, 209, 179, 52, 136, 241, 30,
165, 55, 223, 183, 128, 34, 39, 162, 50, 181, 25, 205, 157,
43, 117, 37, 163, 171, 211, 150, 27, 148, 8, 207, 152, 38,
203, 161, 186, 187, 141, 31, 262, 170, 215, 164, 174, 166, 184,
14, 175, 131, 23], dtype=int64)
Anhand eines Scatterplots prüfen, ob es starke Ausreiser bei der Größe des Geschosses oberhalb des EG (OG_qm) gibt. Falls vorhanden werden diese Einträge aus dem Datensatz entfernt.
# Draw scatterplot with red line to illustrate the outliers
plt.figure(figsize=(16, 7))
sns.scatterplot(data=df_train, x="OG_qm", y="Z_Verkaufspreis")
plt.plot([160, 160], [800000, 0], linewidth=2, color="red")
plt.title("Scatterplot über die unbereinigten OG_qm")
plt.xlabel("Quadratmeter des Geschosses oberhalb des EG")
plt.ylabel("Verkaufspreis in Euro")
Text(0, 0.5, 'Verkaufspreis in Euro')
# Delete all outliers from the data set and display unique entries
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["OG_qm"] <= 160]
df_train_cleaned.loc[:, "OG_qm"].unique()
array([ 0, 73, 72, 65, 77, 52, 58, 75, 79, 83, 74, 119, 59,
62, 31, 49, 71, 42, 96, 47, 48, 70, 63, 91, 39, 44,
64, 54, 154, 100, 108, 61, 53, 87, 45, 29, 88, 133, 80,
78, 35, 57, 51, 67, 69, 56, 76, 43, 68, 82, 25, 46,
66, 50, 156, 26, 85, 86, 103, 110, 89, 105, 36, 113, 99,
60, 55, 38, 84, 97, 81, 93, 106, 41, 111, 98, 136, 90,
125, 137, 109, 17, 92, 28, 27, 138, 102, 32, 94, 37, 33,
40, 159, 112, 116, 14, 95, 118, 104, 115, 130, 34, 19, 146,
12, 10, 18, 15, 30, 129, 152, 23, 122, 117, 20, 9, 114,
21, 151, 121, 107, 101], dtype=int64)
Anhand eines Scatterplots prüfen, ob es starke Ausreiser bei der Größe der gesamten Wohnfläche (Wohnflaeche_qm) gibt. Falls vorhanden werden diese Einträge aus dem Datensatz entfernt.
# Draw scatterplot with red line to illustrate the outliers
plt.figure(figsize=(16, 7))
sns.scatterplot(data=df_train, x="Wohnflaeche_qm", y="Z_Verkaufspreis")
plt.plot([330, 330], [800000, 0], linewidth=2, color="red")
plt.title("Scatterplot über die unbereinigten Wohnflaeche_qm")
plt.xlabel("Wohnfläche in qm")
plt.ylabel("Verkaufspreis in Euro")
Text(0, 0.5, 'Verkaufspreis in Euro')
# Delete all outliers from the data set and display unique entries
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["Wohnflaeche_qm"] <= 330]
df_train_cleaned.loc[:, "Wohnflaeche_qm"].unique()
array([125, 170, 119, 138, 103, 89, 75, 142, 145, 97, 198, 91, 78,
128, 117, 140, 121, 69, 115, 80, 88, 141, 152, 166, 177, 110,
157, 143, 261, 90, 181, 77, 63, 73, 189, 133, 149, 122, 225,
227, 99, 106, 154, 123, 155, 136, 118, 131, 253, 82, 146, 109,
94, 44, 81, 93, 111, 120, 296, 67, 98, 231, 92, 83, 139,
191, 173, 151, 127, 186, 100, 108, 60, 76, 126, 169, 190, 211,
102, 162, 220, 175, 135, 297, 74, 171, 203, 112, 104, 156, 137,
159, 116, 45, 72, 150, 86, 165, 87, 96, 84, 194, 214, 113,
124, 216, 312, 129, 172, 192, 158, 153, 212, 134, 101, 206, 130,
217, 147, 107, 208, 144, 79, 132, 65, 204, 174, 193, 168, 160,
201, 95, 61, 71, 180, 68, 205, 114, 37, 57, 223, 163, 235,
53, 242, 179, 221, 262, 251, 105, 70, 229, 178, 187, 237, 292,
85, 51, 219, 34, 236, 207, 183, 148, 213, 185, 234, 244, 255,
164, 161, 209, 184, 64, 62, 48, 224, 202, 196, 226, 200, 56,
66, 238, 230, 215, 167, 197, 50, 264, 239, 199, 58, 176, 188,
195, 306, 228, 308, 240, 182, 233, 222, 52, 28, 42, 59, 288,
273, 210, 54, 232], dtype=int64)
Es existieren nur zwei "Sehr schlecht" Werte. Diese werden entfernt.
# Delete entries with very poor basement height and display unique entries
df_train_cleaned = df_train_cleaned.loc[df_train_cleaned["Kellerhoehe"] != 1]
df_train_cleaned.loc[:, "Kellerhoehe"].unique()
array([4, 3, 0, 5, 2], dtype=int64)
Die verbleibenden Spalten weisen keine Ausreißer, unzulässige Werte oder sonstige Auffälligkeiten auf. Daher sind keine Bereinigungen erforderlich.
Es sollten Datensätze entfernt werden, bei denen Immobilien früher umgebaut wurden, als ihr Baujahr. Davon gibt es genau einen.
# Show records that match the rule and delete them
rule = (df_train_cleaned["Umgebaut"] < df_train_cleaned["Baujahr"])
display(df_train_cleaned[rule].loc[:, ["Umgebaut", "Baujahr"]])
df_train_cleaned.drop(df_train_cleaned[rule].index, inplace=True)
| Umgebaut | Baujahr | |
|---|---|---|
| 655 | 2013 | 2014 |
Es sollten Datensätze entfernt werden, bei denen die Ausbaustufe größer als 1 ist (Obergeschosse vorhanden) aber der Wert bei OG_qm gleich 0 ist. Davon gibt es 29 Stück.
# Show records that match the rule and delete them
rule = (df_train_cleaned["Ausbaustufe"] > 1) & (df_train_cleaned["OG_qm"] == 0)
display(df_train_cleaned[rule].loc[:, ["Ausbaustufe", "OG_qm"]])
df_train_cleaned.drop(df_train_cleaned[rule].index, inplace=True)
| Ausbaustufe | OG_qm | |
|---|---|---|
| 113 | 2 | 0 |
| 256 | 2 | 0 |
| 333 | 2 | 0 |
| 342 | 2 | 0 |
| 371 | 2 | 0 |
| 548 | 2 | 0 |
| 695 | 2 | 0 |
| 768 | 2 | 0 |
| 820 | 2 | 0 |
| 836 | 2 | 0 |
| 920 | 2 | 0 |
| 949 | 2 | 0 |
| 1030 | 2 | 0 |
| 1157 | 2 | 0 |
| 1243 | 2 | 0 |
| 1282 | 2 | 0 |
| 1348 | 2 | 0 |
| 1352 | 2 | 0 |
| 1492 | 2 | 0 |
| 1539 | 2 | 0 |
| 1559 | 2 | 0 |
| 1576 | 2 | 0 |
| 1620 | 2 | 0 |
| 1689 | 2 | 0 |
| 1767 | 2 | 0 |
| 1799 | 2 | 0 |
| 2043 | 2 | 0 |
| 2285 | 2 | 0 |
| 2334 | 2 | 0 |
Es sollten Datensätze entfernt werden, bei denen die Anzahl der Garagen die Anzahl an Zimmern um den doppelten Wert übersteigt. Davon gibt es genau 2.
# Show records that match the rule and delete them
rule = (df_train_cleaned["AnzahlZimmer"] * 2 < df_train_cleaned["Garagen"])
display(df_train_cleaned[rule].loc[:, ["AnzahlZimmer", "Garagen"]])
df_train_cleaned.drop(df_train_cleaned[rule].index, inplace=True)
| AnzahlZimmer | Garagen | |
|---|---|---|
| 1564 | 1 | 3 |
| 2266 | 1 | 3 |
Es sollten Datensätze entfernt werden, bei denen die Anzahl der Bäder im Keller höher ist als in den Obergeschossen. Davon gibt es genau 3.
# Show records that match the rule and delete them
rule = (df_train_cleaned["Baeder"] < df_train_cleaned["BaederKG"])
display(df_train_cleaned[rule].loc[:, ["Baeder", "BaederKG"]])
df_train_cleaned.drop(df_train_cleaned[rule].index, inplace=True)
| Baeder | BaederKG | |
|---|---|---|
| 557 | 1 | 2 |
| 2222 | 1 | 2 |
| 2278 | 1 | 2 |
Es sollten Datensätze entfernt werden, bei denen die Anzahl der Bäder im Obergeschoss sechs ist. Davon gibt es genau 4.
Anmerkung: Es gibt keine Einträge mit 5 Bädern im Obergeschoss.
# Show records that match the rule and delete them
rule = (df_train_cleaned["Baeder"] == 6)
display(df_train_cleaned[rule].loc[:, ["Baeder"]])
df_train_cleaned.drop(df_train_cleaned[rule].index, inplace=True)
| Baeder | |
|---|---|
| 244 | 6 |
| 599 | 6 |
| 1146 | 6 |
| 1354 | 6 |
Es sollten Datensätze entfernt werden, bei denen die Anzahl der Zimmer acht ist. Davon gibt es genau 1.
Anmerkung: Es gibt keine Einträge mit 7 Zimmern.
# Show records that match the rule and delete them
rule = (df_train_cleaned["AnzahlZimmer"] == 8)
display(df_train_cleaned[rule].loc[:, ["AnzahlZimmer"]])
df_train_cleaned.drop(df_train_cleaned[rule].index, inplace=True)
| AnzahlZimmer | |
|---|---|
| 1808 | 8 |
Anhand der Korrelationsmatrix aus Aufgabe 2 soll abschließend untersucht werden, wie sich die Data Prepatation Schritte auf die Qualität der Korrelation zwischen Attributen und Verkaufspreis ausgewirkt haben.
# Draw a correlation matrix heatmap and mask out the upper triangle
plt.figure(figsize=(20, 10))
mask = np.triu(np.ones_like(df_train_cleaned.iloc[:, 0:20].corr(), dtype=bool))
heatmap = sns.heatmap(df_train_cleaned.iloc[:, 0:20].corr(), mask=mask, vmin=-1, vmax=1, annot=True)
plt.title('Korrelations Heatmap mit allen Features', fontdict={'fontsize': 20})
Text(0.5, 1.0, 'Korrelations Heatmap mit allen Features')
Kellerhoehe).Aufgabenstellung: _Führen Sie mit einem geeigneten Verfahren der linearen Regression eine Vorhersage des Preises (ZVerkaufspreis) durch. Ggfs. brauchen Sie dafür mehrere Versionen der „einfachen“ Regressionslösungen, um eine akzeptable Performance zu erreichen. Erklären Sie wichtige identifizierten Zusammenhänge menschenverständlich als Text (z. B. „Eine Haustür erhöht den Preis um 2,75 EUR.“).
from sklearn.linear_model import LinearRegression, Lasso, Ridge
from sklearn.model_selection import cross_validate
from sklearn.metrics import r2_score
In dieser Funktion wird die Cross-Validation des Models mit 10-fach Faltung ausgeführt. Der Scoring="r2" Parameter gibt an, welcher Wert für die Bewertung verwendet werden soll. R² ist zwar Standard, aber mit expliziter Angabe ist es eindeutiger und besser verständlich. Die Funktion gibt danach den Score für jede einzelne Kreuzvalidierung aus und berechnet den Durchschnitt von allen Durchläufen. Weitere Infos folgen im Abschnitt "Die Auswahl der reduzierten Features".
def cross_validate_model(model, x, y):
show_decimals = 8
validation = cross_validate(model, x, df_train_cleaned[y], cv=10, scoring="r2")
print("Cross-Validation:")
for score in validation['test_score']:
print('\t' + str(np.round(score, decimals=show_decimals)))
print('Mean of cross validation set: ' + str(np.round(np.mean(validation['test_score']), decimals=show_decimals)))
print('With a standard deviation of: ' + str(np.round(np.std(validation['test_score']), decimals=show_decimals)))
Es wird zuerst das Model kreuz-validiert, danach wird es gefittet. Das fitten ist notwendig, um händisch den R² Wert des Modells zu berechnen, wenn es auf den gesamten Daten trainiert wird. Außerdem ist das fitten notwendig, um später in get_coefs() Wertsteigerung pro einer Einheit eines Features berechnen zu können.
def do_regression(model, x, y):
cross_validate_model(model, x, y_fields)
model.fit(x, y)
y_predict = pd.DataFrame(model.predict(x).astype(int), columns=['Prediction'])
r2 = r2_score(y_true=df_train_cleaned['Z_Verkaufspreis'], y_pred=y_predict)
print("R^2 of whole set: " + str(r2))
return model
Hier wird die Wertsteigerung, welche eine Einheit eines Features ausmacht, berechnet und ausgegeben. Es werden alle Features verwendet, um einen gesamten Überblick zu gewähren. Zur genaueren Auswertung folgt unten ein weiter Text.
def get_coefs(model, x, y):
coefficients = pd.concat([pd.DataFrame(x.columns),pd.DataFrame(np.transpose(model.coef_))], axis = 1)
print(coefficients)
Um die Feature-Kombinationen untereinander vergleichen zu können, wurde die Funktion cross_validate_model verwendet. Diese cross-validiert mit einer 10-fachen Faltung. Die jeweiligen R^2 Werte wurden dann im Durchschnitt genommen. Die einzelnen Werte jeder Faltung geben an, wie sehr die Daten sich unterscheiden. Man kann daraus also ablesen, wie sehr die Einteilung in Trainings- und Testdaten sich auf die Genauigkeit der Prognose auswirken. Der Durchschnitt daraus ist dann sinnvoll, um jede entstandene Einteilungskombination der Daten gleich zu gewichten. Gegenüber dem R^2-Wert, der in do_regression auf das gesamte Model berechnet wird hat dies als Vorteil, dass bei der Crossvalidierung mit Daten getestet wird, welche das Modell nicht zum trainieren verwendete. Bei der simplen R^2 Berechnung in do_regression() ist dies nicht so, da alle Daten für das Training verwendet wurden. Darum ist der Durchschnitt der Cross-Validierung als Parameter für die Featureauswahl gewählt worden.
Das Vorgehen für die Feature-Reduktion war, mit allen Features anzufangen (Ohne Nominalskalen) und dann vereinzelte Features wegzulassen. Für diese Auswahl wurde sich grob an der Korrelationsheatmap mit allen Features orientiert (s.o.). Wirkte sich ein Feature sehr auf die Genauigkeit aus, wurde es behalten. Dokumentiert sind nachfolgend einige Zwischenstufen, um einen groben Verlauf darzustellen. Theoretisch wäre auch möglich, jede Kombination auszuprobieren und die Kombination mit der höchsten Genauigkeit zu wählen. Da dies 262143 Kombinationen darstellt, wurde auf diesen Rechenaufwand verzichtet (Alle Binomialkoeffizienten von 18 über 1 bis hin zu 18 über 18 aufsummiert).
Es wurde dabei von Anfang an der Verkaufspreis aus den Daten entfernt, da das Modell diesen vorher sagen soll und nicht zur Vorhersage verwenden soll.
Alle Features:
["AnzahlZimmer","Ausbaustufe","Baeder","BaederKG","Baujahr","EG_qm","Garage_qm","Garagen","Gesamteindruck",
"Keller_Typ_qm","Keller_qm","Kellerhoehe","Kellertyp","Lage","OG_qm","Umgebaut","Verkaufsjahr","Verkaufsmonat","Wohnflaeche_qm"]:
0.80156342
Ohne Nominalskalen:
["AnzahlZimmer","Ausbaustufe","Baeder","BaederKG","Baujahr","EG_qm","Garage_qm","Garagen","Gesamteindruck",
"Keller_Typ_qm","Keller_qm","Kellerhoehe",
"Kellertyp"
,"OG_qm","Umgebaut","Verkaufsjahr","Verkaufsmonat","Wohnflaeche_qm"]:
0.79685377
["AnzahlZimmer","Baeder","Baujahr","EG_qm","Garage_qm","Garagen","Keller_Typ_qm","Keller_qm","Kellertyp","OG_qm","Umgebaut","Wohnflaeche_qm"]:
0.78037179
["AnzahlZimmer","Baeder","Baujahr","Garagen","Keller_Typ_qm","Keller_qm","OG_qm","Umgebaut","Wohnflaeche_qm"]:
0.77778304
["AnzahlZimmer","Baujahr","Garagen","Keller_Typ_qm","Keller_qm","Umgebaut","Wohnflaeche_qm"]:
0.77739578
Das Feature 'Lage' wurde gestrichen, da es sich hier um eine Nominalskala handelt, es kann also an der Skala nicht abgelesen werden, was besser ist. Da lineare Regression allerdings eine Rangordnung benötigt, (also mind. Ordinalskala) kann dieser Wert hierfür nicht verwendet werden. In "Alle Features" ist er für die Vollständigkeit allerdings trotzdem enthalten.
Es wurde sich schlussendlich für die letzten Features entschieden, da sie nicht weiter reduzierbar sind, ohne die Genauigkeit zu sehr einzuschränken. Als Bemerkung hierzu kann noch gesagt werden, dass dies vermutlich nicht die beste, aber eine sehr gute Selektion ist (da sie mit wenig Features auskommt).
x_fields_all = ["AnzahlZimmer","Ausbaustufe","Baeder","BaederKG","Baujahr","EG_qm","Garage_qm","Garagen","Gesamteindruck","Keller_Typ_qm","Keller_qm","Kellerhoehe","Kellertyp","Lage","OG_qm","Umgebaut","Verkaufsjahr","Verkaufsmonat","Wohnflaeche_qm"]
x_fields_reduced = ["AnzahlZimmer","Baujahr","Garagen","Keller_Typ_qm","Keller_qm","Umgebaut","Wohnflaeche_qm"]
y_fields = "Z_Verkaufspreis"
x_all = df_train_cleaned[x_fields_all]
x_reduced = df_train_cleaned[x_fields_reduced]
y = df_train_cleaned[y_fields]
_ = do_regression(LinearRegression(), x_reduced, y)
Cross-Validation: 0.75974436 0.77755587 0.77246763 0.8265534 0.79764716 0.78171757 0.77757536 0.77925664 0.74898027 0.75681353 Mean of cross validation set: 0.77783118 With a standard deviation of: 0.0210091 R^2 of whole set: 0.7822593404038786
Die Werte der einzelnen Aufteilung der Cross-Validierung ähneln sich recht stark, die Standard-Abweichung ist relativ niedrig. Dadurch kann der Durchschnitt als aussagekräftig angesehen werden.
model_for_coefs = do_regression(LinearRegression(), x_all, y)
Cross-Validation: 0.79960074 0.81801552 0.7793978 0.83869302 0.82481669 0.79050262 0.77595845 0.81621057 0.77765918 0.78757471 Mean of cross validation set: 0.80084293 With a standard deviation of: 0.02107026 R^2 of whole set: 0.8082182282586385
Die Werte sind genau wie bei der Regression mit reduzierten Features ohne starke Ausreißer um den Durchschnitt verteilt und die Standard-Abweichung ist auch relativ gering. Die ungefähren 2,5% Einbuße in der Leistung werden hingenommen im Austausch dafür, die Features von 19 auf 7 reduziert zu haben.
get_coefs(model_for_coefs, x_all, y)
0 0 0 AnzahlZimmer -10554.322039 1 Ausbaustufe -1526.172840 2 Baeder -2148.240667 3 BaederKG 1992.051578 4 Baujahr 538.723456 5 EG_qm 659.492957 6 Garage_qm 598.613644 7 Garagen 1200.341278 8 Gesamteindruck 9842.043315 9 Keller_Typ_qm 235.991827 10 Keller_qm 366.605781 11 Kellerhoehe 6213.673213 12 Kellertyp -476.818357 13 Lage -816.349107 14 OG_qm 594.040206 15 Umgebaut 414.885788 16 Verkaufsjahr 5198.348060 17 Verkaufsmonat -842.774424 18 Wohnflaeche_qm 396.922499
In dieser Metrik kann man nun die Gewichtung einzelner Features im erhaltenen Modell erkennen. Man kann bspw. sagen eine Garage erhöht den durchschnittlichen Verkaufspreis um 1333€. Auffällige Werte hier sind:
Anzahl Zimmer: Dieser relativ hohe Wert pro Zimmer wirkt kontraintuitiv, da eigentlich ein Haus mit mehr Zimmern teurer sein müsste. Aus dem Plot geht hervor, das Häuser mit vielen Zimmern (5 und 6) geringere Maximalpreise erzielen. Dadurch diese negative Korrelation.
Ausbaustufe: Hier ziehen die Häuser mit Ausbaustufe 3 die Vorhersage so stark runter, das eine negative Korrelation entsteht.
Baeder: Bei dem Wert der Bäder kann man vermuten, dass hier eine negative Korrelation entsteht aufgrund der Verteilung der Bäderanzahl. Es gibt sehr viele Immobilien mit zwei Bädern, dadurch fällt dies wahrscheinlich so viel mehr ins Gewicht, dass es zu der negativen Korrelation kommt.
Kellertyp: Da es sich bei diesem Feature um keine Ordinalskala handelt, ist der Wert nicht aussagekräftig.
Lage: Da es sich bei diesem Feature um keine Ordinalskala handelt, ist der Wert nicht aussagekräftig.
Verkaufsmonat: Es ist zwar ein Zusammenhang (auch im Diagramm) zu erkennen, allerdings wird hier nicht weiter darauf eingegangen, da vermutet wird, das es sich um keinen kausalen Zusammenhang zwischen Verkaufsmonat und Verkaufspreis handelt. (Siehe auch Korrelationsmatrix oben, sie Korrelieren mit -0.043)
sns.countplot(data=df_train_cleaned, x="Baeder")
plt.title("Verteilung der Garagenanzahl über den Datensatz")
plt.show()
plt.figure(figsize=(20, 10))
plt.subplot(2,2,1)
sns.violinplot(data=df_train_cleaned, x="AnzahlZimmer", y="Z_Verkaufspreis")
plt.title("Violinplot Anzahl Zimmer gegen Verkaufspreis")
plt.subplot(2,2,2)
sns.violinplot(data=df_train_cleaned, x="Ausbaustufe", y="Z_Verkaufspreis")
plt.title("Violinplot Ausbaustufe gegen Verkaufspreis")
plt.subplot(2,2,3)
sns.violinplot(data=df_train_cleaned, x="Baeder", y="Z_Verkaufspreis")
plt.title("Violinplot Anzahl Baeder gegen Verkaufspreis")
plt.subplot(2,2,4)
sns.violinplot(data=df_train_cleaned, x="Verkaufsmonat", y="Z_Verkaufspreis")
plt.title("Violinplot Verkaufsmonat gegen Verkaufspreis")
plt.plot()
[]
Aufgabenstellung: _Vergleichen und optimieren Sie ein oder
mehrere weitere Verfahren zur Vorhersage des Verkaufspreises. Gehen Sie vor wie
in der Vorlesung gelehrt mit Trainings- und Validierungsdaten (80-20). Optimieren
Sie Ihre Vorhersage wenn sinnvoll.
Geben Sie für den Trainings- und Validierungsdatensatz die Zielwerte R2, MSE,
RMSE, MAPE, MAX aus. Dokumentieren Sie dies auch.
Interpretieren Sie das Ergebnis und den Einfluss der Features (falls möglich).
Untersuchen Sie Varianz und Verzerrung in der Vorhersage.
Schreiben Sie in die data_for_test.csv die auf Basis Ihres besten Modells
vorhergesagt Werte in eine neue Spalte und geben Sie diese Datei mit ab. (Hinweis:
Sortieren Sie nicht um)._
Die gelabelten Daten (data_for_training.csv) werden im Verhältnis 80:20 in Trainings- und Validierungsdaten aufgeteilt.
Die Parameterauswahl erfolgt anhand der Ergebnisse aus den Aufgaben 3 und 4. Es werden folgende Attribute für die Vorhersage des Verkaufspreises gewählt:
"Baeder", "Baujahr", "EG_qm", "Garage_qm", "Garagen", "Keller_qm", "Kellerhoehe", "Umgebaut", "Wohnflaeche_qm"
from sklearn.model_selection import train_test_split
# Select parameters
#selected_columns_old = ["Baeder", "Baujahr", "EG_qm", "Garage_qm", "Garagen", "Keller_qm", "Kellerhoehe", "Umgebaut", "Wohnflaeche_qm", "Z_Verkaufspreis"]
selected_columns = ["AnzahlZimmer", "Baeder", "Baujahr", "EG_qm", "Garage_qm", "Keller_Typ_qm", "Keller_qm", "Wohnflaeche_qm", "Umgebaut", "Verkaufsjahr", "Verkaufsmonat", "Z_Verkaufspreis"]
df_train_selected_features = df_train_cleaned.loc[:, selected_columns]
# Split the data in input features (X) and target values (Y)
X = df_train_selected_features.drop(columns=["Z_Verkaufspreis"])
Y = df_train_selected_features["Z_Verkaufspreis"]
# Split data set into training and validation data
x_train, x_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=42)
Nun werden verschiedene Verfahren zur Vorhersage des Verkaufspreises verglichen. Das Verfahren mit der besten Vorhersage wird dann weiter optimiert und auf die Testdaten (data_for_test.csv) angewendet. Diese Verfahren werden im folgenden angewendet und verglichen:
Für alle Verfahren werden auch die zugehörigen Fehlermetriken berechnet. Mit Hilfe von Fehlermetriken ist es möglich, die Güte eines Modells zu quantifizieren. Durch sie ist es möglich, die Eignung von Modellen für bestimmte Aufgaben objektiv zu bewerten. Dazu werden die folgenden Fehlermetriken herangezogen:
Die Wichtigkeit eines Merkmals (feature importance) wird als die normierte Gesamtreduktion des Kriteriums durch dieses Merkmal berechnet. Sie ist auch als Gini-Bedeutung bekannt. Je höher, desto wichtiger ist das Merkmal.
from sklearn.metrics import r2_score, mean_absolute_percentage_error, mean_squared_error, max_error
# Function to calculate and output the error metrics
def output_error_metrics(y_test, y_prediction):
print(f"R2:\t{np.around(r2_score(y_test, y_prediction), decimals=4)}")
print(f"MSE:\t{np.around(mean_squared_error(y_test, y_prediction), decimals=4)}")
print(f"RMSE:\t{np.around(np.sqrt(mean_squared_error(y_test, y_prediction)), decimals=4)}")
print(f"MAPE:\t{np.around(mean_absolute_percentage_error(y_test, y_prediction), decimals=4)}")
print(f"MAX:\t{np.around(max_error(y_test, y_prediction), decimals=4)}")
# Function to output scatterplots with regression lines
def output_regression_scatterplots(y_prediction):
fig, axes = plt.subplots(1, len(selected_columns)-1, figsize=(35, 5), sharey=True)
axes[2].set_title("Calculated regression lines over the selected features")
axes[0].set_ylabel("Predicted sales price")
# Draw the individual scatterplots based on the selected attribute columns
selected_columns_without_price = selected_columns[0:(len(selected_columns)-1)]
for col in selected_columns_without_price:
sns.regplot(x=col, y=y_prediction, data=x_train, ax=axes[selected_columns_without_price.index(col)], line_kws={"color":"k"})
plt.show()
# Function for the execution of the various procedures
def perform_prediction_and_evaluate_results(regr, output_feature_importance = True):
regr.fit(x_train, y_train)
print("Evaluation on train data:")
y_train_prediction = regr.predict(x_train)
output_error_metrics(y_train, y_train_prediction)
print("\nEvaluation on test data:")
y_test_prediction = regr.predict(x_test)
output_error_metrics(y_test, y_test_prediction)
if output_feature_importance == True:
print("\nFeature importances:")
feature_names = regr.feature_names_in_
feature_importances = regr.feature_importances_
for i in range(regr.n_features_in_):
if feature_names[i] == "Baeder" or feature_names[i] == "EG_qm":
print(f"{feature_names[i]}:\t\t{round(feature_importances[i], 4)}")
else:
print(f"{feature_names[i]}:\t{round(feature_importances[i], 4)}")
print("\nScatterplots with regression lines:")
output_regression_scatterplots(y_train_prediction)
Nun wird eine Random Forest Regression mit den zuvor reduzierten Attributen durchgeführt. Die Bewertung erfolgt anhand der vorgegebenen Fehlermetriken.
from sklearn.ensemble import RandomForestRegressor
randomForest = RandomForestRegressor(n_estimators=100)
perform_prediction_and_evaluate_results(randomForest)
Evaluation on train data: R2: 0.9759 MSE: 130418192.1761 RMSE: 11420.0785 MAPE: 0.0472 MAX: 87702.28 Evaluation on test data: R2: 0.8582 MSE: 888317865.7749 RMSE: 29804.6618 MAPE: 0.1199 MAX: 137773.97 Feature importances: AnzahlZimmer: 0.0096 Baeder: 0.0052 Baujahr: 0.3979 EG_qm: 0.0687 Garage_qm: 0.1482 Keller_Typ_qm: 0.0452 Keller_qm: 0.0837 Wohnflaeche_qm: 0.1755 Umgebaut: 0.0308 Verkaufsjahr: 0.0205 Verkaufsmonat: 0.0147 Scatterplots with regression lines:
Nun wird eine Extra Trees Regression mit den zuvor reduzierten Attributen durchgeführt. Die Bewertung erfolgt anhand der vorgegebenen Fehlermetriken.
from sklearn.ensemble import ExtraTreesRegressor
extraTrees = ExtraTreesRegressor(n_estimators=100)
perform_prediction_and_evaluate_results(extraTrees)
Evaluation on train data: R2: 1.0 MSE: 2.1942 RMSE: 1.4813 MAPE: 0.0 MAX: 40.0 Evaluation on test data: R2: 0.8703 MSE: 812519065.1808 RMSE: 28504.7201 MAPE: 0.1159 MAX: 139277.75 Feature importances: AnzahlZimmer: 0.0207 Baeder: 0.0871 Baujahr: 0.1527 EG_qm: 0.0918 Garage_qm: 0.0994 Keller_Typ_qm: 0.0489 Keller_qm: 0.1289 Wohnflaeche_qm: 0.2028 Umgebaut: 0.1223 Verkaufsjahr: 0.0286 Verkaufsmonat: 0.0168 Scatterplots with regression lines:
Nun wird eine Gradient Boosting Regression mit den zuvor reduzierten Attributen durchgeführt. Die Bewertung erfolgt anhand der vorgegebenen Fehlermetriken.
from sklearn.ensemble import GradientBoostingRegressor
gradientBoosting = GradientBoostingRegressor(n_estimators=100)
perform_prediction_and_evaluate_results(gradientBoosting)
Evaluation on train data: R2: 0.9065 MSE: 505162073.8941 RMSE: 22475.8109 MAPE: 0.1018 MAX: 115597.5334 Evaluation on test data: R2: 0.8486 MSE: 948449267.9183 RMSE: 30796.9035 MAPE: 0.1222 MAX: 182562.9342 Feature importances: AnzahlZimmer: 0.0037 Baeder: 0.0193 Baujahr: 0.339 EG_qm: 0.0669 Garage_qm: 0.1355 Keller_Typ_qm: 0.0437 Keller_qm: 0.1231 Wohnflaeche_qm: 0.2004 Umgebaut: 0.0491 Verkaufsjahr: 0.0179 Verkaufsmonat: 0.0013 Scatterplots with regression lines:
Die drei auf Suchbäumen basierenden Regressionsverfahren eignen sich mit einer Genauigkeit von ca. 84 % bis 87 % (R2-Wert) im Testdatensatz gut für die Vorhersage des Verkaufspreises. Durch die geplotteten Graphen wird ebenfalls deutlich, dass die Daten dicht an der angepassten Regressionslinie liegen. An den MSE- und RMSE-Werten fällt auf, dass Varianz und Verzerrung bei allen drei Verfahren recht ähnlich sind. Am geringsten sind sie bei der Extra Trees Regression. Der größte Fehler zwischen dem vorhergesagten Wert und dem wahren Wert (MAX) liegt bei Gradient Boosting vor.
Die Bedeutung und der Einfluss der einzelnen Attribute (Gini-Bedeutung) unterscheidet sich ebenfalls stark von Verfahren zu Verfahren. Bei Random Forest stehen Baujahr (0,40), Wohnflaeche_qm (0,17), Garage_qm (0,15) und Keller_qm (0,09) im Vordergrund. Bei Extra Trees ist die Gewichtung gleichmäßiger über alle Attribute verteilt. Am bedeutensten sind Wohnflaeche_qm (0,18), Baujahr (0,17) und Garage_qm (0,13). Bei der Gradient Boosting Regression wird die Gewichtung durch Baujahr (0,34), Wohnflaeche_qm (0,20), Garagen_qm (0,14) und Keller_qm (0,12) angeführt. Auffällig ist hier auch dass die Zimmeranzahl mit 0,0034 und der Verkaufsmonat mit 0,002 so gut wie nicht in die Vorhersagenberechnung einbezogen werden.
Nun wird eine Epsilon-Support Vector Regression mit den zuvor reduzierten Attributen durchgeführt. Die Bewertung erfolgt anhand der vorgegebenen Fehlermetriken.
from sklearn.svm import SVR
svr = SVR(kernel="linear")
perform_prediction_and_evaluate_results(svr, output_feature_importance = False)
Evaluation on train data: R2: 0.7553 MSE: 1322232875.6552 RMSE: 36362.5202 MAPE: 0.1453 MAX: 233983.6639 Evaluation on test data: R2: 0.784 MSE: 1353246258.6465 RMSE: 36786.4956 MAPE: 0.1343 MAX: 224341.2783 Scatterplots with regression lines:
Nun wird eine Passive Aggressive Regressor Regression mit den zuvor reduzierten Attributen durchgeführt. Die Bewertung erfolgt anhand der vorgegebenen Fehlermetriken.
from sklearn.linear_model import PassiveAggressiveRegressor
passiveAgressive = PassiveAggressiveRegressor()
perform_prediction_and_evaluate_results(passiveAgressive, output_feature_importance = False)
Evaluation on train data: R2: 0.3004 MSE: 3779908525.846 RMSE: 61480.9607 MAPE: 0.3096 MAX: 257078.4659 Evaluation on test data: R2: 0.3854 MSE: 3849843582.8979 RMSE: 62047.1078 MAPE: 0.3041 MAX: 239532.948 Scatterplots with regression lines:
Die beiden Verfahren Epsilon-Support Vector Regression und Passive Aggressive Regression schneiden im direkten Vergleich zu den vorherigen drei Verfahren signifikant schlechter ab. Die R2-Werte betragen 0,75 bei Epsilon-Support Vector und 0,63 bei Passive Aggressive. Die Performance auf den Testdaten ist allerdings beides mal besser, als auf den Trainingsdaten. An den RMSE-Werten sieht man, dass die durchschnittliche Distanz zwischen prognostiziertem und tatsächlichem Verkaufspreis (in Euro) deutlich zugenommen hat. Von durchschnittlichen 33.310 € bei den suchbaumbasierten Regressionsverfahren, steigt der Wert um 5.641 € bei Epsilon-Support Vector und um ganze 13.590 € bei Passive Aggressive.
In unserem Fall hat sich die Extra Trees Regression als geeignetste Methode für die Vorhersage des Verkaufspreises herausgestellt. Um die Vorhersage weiter zu optimieren, wird nun eine Hyperparameteroptimierung durchgeführt. Dafür stellt sklearn die Methode GridSearchCV bereit.
Da die Hyperparameteroptimierung über die Funktion GridSearchCV eine rechen- und zeitintensive Operation ist, ist sie im Notebook standardmäßig deaktiviert. Der durchschnittliche Score beträgt 0,85.
from sklearn.ensemble import ExtraTreesRegressor
from sklearn.model_selection import GridSearchCV
# Activate/Deactivate GridSearch function
gridSearch_active = False
gridSearch_repeats = 3
# Calculate optimal hyperparameters
if gridSearch_active == True:
for i in range(gridSearch_repeats):
extraTrees = ExtraTreesRegressor()
grid_search = GridSearchCV(extraTrees, {
# Dictionary with parameter names as keys and lists of settings to try as values
"max_depth": [21, 22, 23, 24, 25, 26, 27, 28],
"min_samples_split": [2, 3, 4, 5, 6, 7, 8, 9, 10],
"n_estimators": [150, 155, 160, 165, 170, 175, 180, 185, 190, 195, 200]
}, n_jobs=-1, verbose=1)
grid_search.fit(X, Y)
# Output results
print("The best parameters are:")
for key, value in grid_search.best_params_.items():
print(f" • {key}: {value}")
print("The score is:", grid_search.best_score_)
Beispielausgabe:
Fitting 5 folds for each of 792 candidates, totalling 3960 fits
The best parameters are:
• max_depth: 25
• min_samples_split: 5
• n_estimators: 170
The score is: 0.8498264789384029
Fitting 5 folds for each of 792 candidates, totalling 3960 fits
The best parameters are:
• max_depth: 27
• min_samples_split: 5
• n_estimators: 190
The score is: 0.8497874589933844
Fitting 5 folds for each of 792 candidates, totalling 3960 fits
The best parameters are:
• max_depth: 27
• min_samples_split: 5
• n_estimators: 150
The score is: 0.8501926503521723
Nachdem die optimalen Hyperparameter berechnet wurden, wird das ausgewählte Modell abschließend mit dem kompletten Datensatz (Trainings- + Validierungsdaten) final trainiert. Dadurch werden die vorhandenen Informationen bestmöglich ausgenutzt. Die Vorhersagen des neuen Modells sind mindestens so gut, wie die des Vorherigen, können aber auch besser sein.
from sklearn.ensemble import ExtraTreesRegressor
# Create the gradient boosting regressor with the selected hyperparameters
extraTrees_final = ExtraTreesRegressor(
random_state=0,
max_depth=25,
min_samples_split=5,
n_estimators=170
)
# Training the model
extraTrees_final.fit(X, Y)
ExtraTreesRegressor(max_depth=25, min_samples_split=5, n_estimators=170,
random_state=0)In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. ExtraTreesRegressor(max_depth=25, min_samples_split=5, n_estimators=170,
random_state=0)data_for_test_filled.csv schreiben¶Mit dem finalen Modell werden nun Verkaufspreise für den Testdatensatz vorhergesagt. Anschließend werden die Vorhersagen in das data_for_test_filled.csv File geschrieben.
# Select chosen parameters on the test data frame
#selected_columns_old = ["Baeder", "Baujahr", "EG_qm", "Garage_qm", "Garagen", "Keller_qm", "Kellerhoehe", "Umgebaut", "Wohnflaeche_qm"]
selected_columns = ["AnzahlZimmer", "Baeder", "Baujahr", "EG_qm", "Garage_qm", "Keller_Typ_qm", "Keller_qm", "Wohnflaeche_qm", "Umgebaut", "Verkaufsjahr", "Verkaufsmonat"]
df_test_selected_features = df_test.loc[:, selected_columns]
# Define mapping and apply labelencoding for the required columns in the test dataset
#mapping = {"0": 0, "Sehr Schlecht": 1, "Schlecht": 2, "Durchschnitt": 3, "Gut": 4, "Sehr gut": 5}
#df_test_selected_features["Kellerhoehe"] = df_test_selected_features["Kellerhoehe"].replace(mapping)
# Make predictions for the "data_for_test.csv" data with the trained model
y_predicted_final = extraTrees_final.predict(df_test_selected_features)
# Read data_for_test, add the predictions to the corresponding rows and save as a new CSV file
try:
# Create new file and open test and test_filled
data_for_test_filled = open("data_for_test_filled.csv", "x").close()
data_for_test = open("data_for_test.csv", "r")
data_for_test_filled = open("data_for_test_filled.csv", "a")
# Loop through every row and append the prediction at the end of the line
# The first line is a special case
first_row = True
count_rows = df_test_selected_features.shape[0]
for row, i in zip(data_for_test, range(-1, count_rows)):
if first_row == True:
data_for_test_filled.write(row.removesuffix("\n") + ";" + "Z_Verkaufspreis" + "\n")
first_row = False
else:
data_for_test_filled.write(row.removesuffix("\n") + ";" + str(int(np.round(y_predicted_final[i]))) + "\n")
# Close both files
data_for_test.close()
data_for_test_filled.close()
print("File data_for_test_filled.csv created successfully.")
except FileExistsError:
print("The file data_for_test_filled.csv does already exist. No changes were made.")
The file data_for_test_filled.csv does already exist. No changes were made.
Aufgabenstellung: Erstellen Sie eine Anleitung oder Handreichung für die in Aufgabe 1 genannte Zielgruppe. Dies soll aus Zielgruppensicht wichtige Erkenntnisse der Aufgaben 2 bis 5 zusammenfassen und maximal 2 Seiten im pdf-Ausdruck umfassen, welche auf Basis der Texte aus Aufgabe 1 dann komplett eigenständig lesbar sein sollen.
Durch ausführliche Analyse der zur Verfügung stehenden Daten konnten viele Erkenntnisse gewonnen werden. Es kann nun eine Antwort auf die Anfangs gestellten Fragen gegeben werden. Um den Spekulant*innen eine grobe Bewertung der Attraktivität der Angebote zu geben, folgen hier nun Einsichten, welche aus den Daten gewonnen wurden.
Die Liste gibt alle Attribute an, welche den Verkaufspreis erhöhen können. Intuitiv sind hier Baujahr, Wohnfläche und das Umgebaut-Datum enthalten. Dabei etwas weniger intuitiv sind die Attribute Garagengröße- und Stellplätze, sowie die Kellerhoehe, welche auch einen hohen Einfluss haben. Die Bäderanzahl und die Quadratmeter des Kellers haben ebenfalls positive Auswirkungen auf den Verkaufspreis, allerdings deutlich geringer als die Attribute aus Liste 1. Es kann sich also an Bäderanzahl und Kellerfläche orientiert werden, allerdings mit geringerer Gewichtung.
Attribute, welche positive Auswirkungen auf den Verkaufspreis haben:
| Attributbezeichnung | Preissteigerung / Einheit |
|---|---|
| Baujahr | 538€ / Jahr |
| Erdgeschossgröße | 659€ / m² |
| Garagengröße | 598€ / m² |
| Garagenstellplätze | 1200€ / Auto-Platz |
| Kellerhoehe | 6213€ / Kategorie |
| Umgebaut | 414€ / Jahr |
| Wohnfläche | 396€ / m² |
Attribute, welche (geringe) Auswirkungen auf den Verkaufspreis haben:
BäderanzahlKeller_qmGegenteilig zu den positiven Auswirkungen auf den Preis haben sich auch Attribute herausgestellt, welche keinen wirklichen Einfluss auf den Preis haben:
Attribute, welche sich kaum direkt auswirken:
Anzahl ZimmerAusbaustufeGesamteindruckKellertypÜberarschend ist hier die Anzahl der Zimmer und der Gesamteindruck. Dieser ist ein subjektiver Wert eines Individuums und ist nur vom äußeren Aussehen der Immobilie abhängig. Dadurch schneiden hier etwas ältere Häuser pauschal immer schlechter ab, obwohl sie nicht billiger sind.
Zur Klassifikation der Attraktivität der Angebote wurde ein Extra Trees Regression KI-Modell erstellt, welches den Preis einer Immobilie aufgrund der Attribute vorhersagt. Dies geschieht dank Optimierung mit 85% Genauigkeit. Wenn man ein neues Angebot einschätzen möchte, nimmt man also den geforderten Preis (Z_Verkaufspreis) und subtrahiert den vorhergesagten Wert der KI davon. Wenn ein negatives Ergebniss herauskommt, also der vorhergesagte Preis höher ist, dann handelt es sich sehr wahrscheinlich um ein gutes Angebot. Wenn ein positives Ergebnis herauskommt, dann ist der eigentliche Wert der Immobilie sehr wahrscheinlich unter ihrem Preis. Je größer die Differenz des Preises und des vorhergesagten Wertes ist, desto besser oder schlechter wird das Angebot.
Lage nicht verwendet, nicht so richtig aussagekräftig
Zusammengefasst sind die Informationen über die Attribute sehr aufschlussreich. Die Unterscheidung zwischen den verschiedenen Stufen der Auswirkung kann beim Einschätzen der Angebote schon sehr helfen. Auffällig ist, wie sehr eine Garage und ihre Fläche den Preis beeinflusst. Auf der gegenteiligen Seite fällt ins Auge, wie wenig die Anzahl der Zimmer mit dem Preis korreliert. Aus der Datenanalyse ging allerdings auch hervor, dass die teuersten Häuser zwischen zwei und vier Zimmer haben. Der Gesamteindruck korreliert ebenfalls wenig mit dem Verkaufspreis. Die teuersten Immobilien haben fast ausnahmslos einen Gesamteindruck von drei erhalten. Es zeigt also, dass dieser Wert nicht viel über den Wert einer Immobilie aussagt.
Beim Verkaufszeitpunkt fällt auf, dass in den letzten Jahren die Preise für Immobilien leicht gestiegen sind. Als Grund dafür werden steigende Immobilienpreise oder Inflation vermutet. Zwischen dem Verkaufsmonat und dem Verkaufspreis besteht auch ein leichter Zusammenhang, hier fällt auf, dass in den Monaten April bis July sehr viele Immobilien verkauft werden, allerdings auch ein wenig teurer als in den restlichen Monaten.
Aufgabenstellung: Versuchen Sie Immobilien dem richtigen Bezirk zuzuordnen, dabei können Sie den Preis als Eingabewert nehmen. Bewerten Sie die Qualität Ihrer Lösung und kommentieren Sie Ihre Erkenntnisse aus diesem kleinen Test. Erwarteter Umfang entspricht 3 Punkten von 30.
#7